package com.ctriposs.baiji.schema; import com.ctriposs.baiji.exception.BaijiRuntimeException; import com.ctriposs.baiji.exception.BaijiTypeException; import com.ctriposs.baiji.util.ObjectUtils; import org.codehaus.jackson.JsonFactory; import org.codehaus.jackson.JsonGenerator; import org.codehaus.jackson.JsonNode; import org.codehaus.jackson.JsonParser; import org.codehaus.jackson.map.ObjectMapper; import org.codehaus.jackson.node.ArrayNode; import java.io.IOException; import java.io.StringWriter; /** * An abstract data type. * <p>A schema may be one of: * <ul> * <li>A <i>record</i>, mapping field names to field value data; * <li>An <i>enum</i>, containing one of a small set of symbols; * <li>An <i>array</i> of values, all of the same schema; * <li>A <i>map</i>, containing string/value pairs, of a declared schema; * <li>A <i>union</i> of other schemas; * <li>A unicode <i>string</i>; * <li>A sequence of <i>bytes</i>; * <li>A 32-bit signed <i>int</i>; * <li>A 64-bit signed <i>long</i>; * <li>A 32-bit IEEE single-<i>float</i>; or * <li>A 64-bit IEEE <i>double</i>-float; or * <li>A <i>boolean</i>; or * <li><i>null</i>. * </ul> * <p/> * methods. The schema objects are <i>logically</i> immutable. */ public abstract class Schema { static final JsonFactory FACTORY = new JsonFactory(); static final ObjectMapper MAPPER = new ObjectMapper(FACTORY); static { FACTORY.enable(JsonParser.Feature.ALLOW_COMMENTS); FACTORY.setCodec(MAPPER); } private final SchemaType _type; private final PropertyMap _props; Schema(SchemaType type, PropertyMap props) { this._type = type; this._props = props; } /** * Return the type of this schema. */ public SchemaType getType() { return _type; } /** * Return additional JSON attributes apart from those defined in the Baiji spec */ public PropertyMap getProps() { return _props; } /** * Returns the schema's custom property value given the property name * * @param key * @return */ public String getProp(String key) { if (null == _props) { return null; } return _props.get(key); } /** * The name of this schema. If this is a named schema such as an enum, * it returns the fully qualified name for the schema. * For other schemas, it returns the type of the schema. * * @return */ public abstract String getName(); /** * Parses a JSON string to create a new schema object * * @param json JSON string * @return */ public static Schema parse(String json) { if (json == null || json.isEmpty()) { throw new IllegalArgumentException("json cannot be null or empty."); } return parse(json.trim(), new SchemaNames(), null); // standalone schema, so no enclosing namespace } /** * Parses a JSON string to create a new schema object * * @param json JSON string * @param names list of named schemas already read * @param encSpace enclosing namespace of the schema * @return */ static Schema parse(String json, SchemaNames names, String encSpace) { Schema schema = PrimitiveSchema.newInstance(json); if (schema != null) { return schema; } try { JsonNode node = MAPPER.readTree(json); return parseJson(node, names, encSpace); } catch (Throwable t) { throw new SchemaParseException("Could not parse. " + t.getMessage() + "\n" + json); } } /** * Static method to return new instance of schema object * * @param node JSON object * @param names list of named schemas already read * @param encSpace enclosing namespace of the schema * @return */ static Schema parseJson(JsonNode node, SchemaNames names, String encSpace) { if (node == null) { throw new IllegalArgumentException("node cannot be null."); } if (node.isTextual()) { String value = node.getTextValue(); PrimitiveSchema ps = PrimitiveSchema.newInstance(value); if (ps != null) { return ps; } NamedSchema schema = names.getSchema(value, null, encSpace); if (schema != null) { return schema; } throw new SchemaParseException("Undefined name: " + value); } else if (node.isArray()) { return UnionSchema.newInstance((ArrayNode) node, null, names, encSpace); } else if (node.isObject()) { JsonNode typeNode = node.get("type"); if (typeNode == null) { throw new SchemaParseException("Property type is required"); } PropertyMap props = JsonHelper.getProperties(node); if (typeNode.isTextual()) { String type = typeNode.getTextValue(); if ("array".equals(type)) { return ArraySchema.newInstance(node, props, names, encSpace); } else if ("map".equals(type)) { return MapSchema.newInstance(node, props, names, encSpace); } PrimitiveSchema ps = PrimitiveSchema.newInstance(type); if (ps != null) { return ps; } return NamedSchema.newInstance(node, props, names, encSpace); } else if (typeNode.isArray()) { return UnionSchema.newInstance((ArrayNode) typeNode, props, names, encSpace); } } throw new BaijiTypeException("Invalid JSON for schema: " + node); } /** * Render this as <a href="http://json.org/">JSON</a>. */ @Override public String toString() { return toString(false); } /** * Render this as <a href="http://json.org/">JSON</a>. * * @param pretty if true, pretty-print JSON. */ public String toString(boolean pretty) { try { StringWriter writer = new StringWriter(); JsonGenerator gen = FACTORY.createJsonGenerator(writer); if (pretty) gen.useDefaultPrettyPrinter(); if (this instanceof PrimitiveSchema || this instanceof UnionSchema) { gen.writeStartObject(); gen.writeFieldName("type"); } writeJson(gen, new SchemaNames(), null); if (this instanceof PrimitiveSchema || this instanceof UnionSchema) { gen.writeEndObject(); } gen.flush(); return writer.toString(); } catch (IOException e) { throw new BaijiRuntimeException(e); } } /** * Writes schema object in JSON format * * @param gen JSON generator * @param names list of named schemas already written * @param encSpace enclosing namespace of the schema */ protected void writeJson(JsonGenerator gen, SchemaNames names, String encSpace) throws IOException { writeStartObject(gen); writeJsonFields(gen, names, encSpace); if (null != _props) { _props.writeJson(gen); } gen.writeEndObject(); } /** * Writes opening { and 'type' property * * @param gen */ private void writeStartObject(JsonGenerator gen) throws IOException { gen.writeStartObject(); gen.writeFieldName("type"); gen.writeString(_type.toString().toLowerCase()); } /** * Default implementation for writing schema properties in JSON format * * @param gen JSON generator * @param names list of named schemas already written * @param encSpace enclosing namespace of the schema */ protected void writeJsonFields(JsonGenerator gen, SchemaNames names, String encSpace) throws IOException { } public boolean equals(Object o) { if (o == this) { return true; } if (!(o instanceof Schema)) { return false; } Schema that = (Schema) o; if (this._type != that._type) { return false; } return _props != null ? _props.equals(that._props) : that._props == null; } public int hashCode() { return _type.hashCode() + ObjectUtils.hashCode(_props); } }